All files / local local_documents_view.ts

98.7% Statements 76/77
87.5% Branches 14/16
100% Functions 25/25
98.51% Lines 66/67
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234                                  2x 2x               2x 2x   2x       2x                 2x   675x 675x                 2248x       2248x     2248x                   1988x       1988x 1988x 1988x 1848x     1848x 104x   1848x       1988x       2x       450x 202x   248x       2x         202x   202x 202x 62x   202x         248x                 248x     248x     248x     248x           248x 248x 26x     26x 26x           248x 248x 26x   26x 20x         248x         248x 96x 8x       248x                         2x         2326x     2326x 904x   2326x                       248x       248x 248x 78x   78x 76x 2x 2x             248x   2x  
/**
 * Copyright 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
import { Query } from '../core/query';
import { SnapshotVersion } from '../core/snapshot_version';
import {
  documentKeySet,
  DocumentKeySet,
  DocumentMap,
  documentMap,
  MaybeDocumentMap,
  maybeDocumentMap
} from '../model/collections';
import { Document, MaybeDocument, NoDocument } from '../model/document';
import { DocumentKey } from '../model/document_key';
import { ResourcePath } from '../model/path';
import { fail } from '../util/assert';
 
import { MutationQueue } from './mutation_queue';
import { PersistenceTransaction } from './persistence';
import { PersistencePromise } from './persistence_promise';
import { RemoteDocumentCache } from './remote_document_cache';
 
/**
 * A readonly view of the local state of all documents we're tracking (i.e. we
 * have a cached version in remoteDocumentCache or local mutations for the
 * document). The view is computed by applying the mutations in the
 * MutationQueue to the RemoteDocumentCache.
 */
export class LocalDocumentsView {
  constructor(
    private remoteDocumentCache: RemoteDocumentCache,
    private mutationQueue: MutationQueue
  ) {}
 
  /**
   * Get the local view of the document identified by `key`.
   *
   * @return Local view of the document or null if we don't have any cached
   * state for it.
   */
  getDocument(
    transaction: PersistenceTransaction,
    key: DocumentKey
  ): PersistencePromise<MaybeDocument | null> {
    return this.remoteDocumentCache
      .getEntry(transaction, key)
      .next(remoteDoc => {
        return this.computeLocalDocument(transaction, key, remoteDoc);
      });
  }
 
  /**
   * Gets the local view of the documents identified by `keys`.
   *
   * If we don't have cached state for a document in `keys`, a NoDocument will
   * be stored for that key in the resulting set.
   */
  getDocuments(
    transaction: PersistenceTransaction,
    keys: DocumentKeySet
  ): PersistencePromise<MaybeDocumentMap> {
    const promises = [] as Array<PersistencePromise<void>>;
    let results = maybeDocumentMap();
    keys.forEach(key => {
      promises.push(
        this.getDocument(transaction, key).next(maybeDoc => {
          // TODO(http://b/32275378): Don't conflate missing / deleted.
          if (!maybeDoc) {
            maybeDoc = new NoDocument(key, SnapshotVersion.forDeletedDoc());
          }
          results = results.insert(key, maybeDoc);
        })
      );
    });
    return PersistencePromise.waitFor(promises).next(() => results);
  }
 
  /** Performs a query against the local view of all documents. */
  getDocumentsMatchingQuery(
    transaction: PersistenceTransaction,
    query: Query
  ): PersistencePromise<DocumentMap> {
    if (DocumentKey.isDocumentKey(query.path)) {
      return this.getDocumentsMatchingDocumentQuery(transaction, query.path);
    } else {
      return this.getDocumentsMatchingCollectionQuery(transaction, query);
    }
  }
 
  private getDocumentsMatchingDocumentQuery(
    transaction: PersistenceTransaction,
    docPath: ResourcePath
  ): PersistencePromise<DocumentMap> {
    // Just do a simple document lookup.
    return this.getDocument(transaction, new DocumentKey(docPath)).next(
      maybeDoc => {
        let result = documentMap();
        if (maybeDoc instanceof Document) {
          result = result.insert(maybeDoc.key, maybeDoc);
        }
        return result;
      }
    );
  }
 
  private getDocumentsMatchingCollectionQuery(
    transaction: PersistenceTransaction,
    query: Query
  ): PersistencePromise<DocumentMap> {
    // Query the remote documents and overlay mutations.
    // TODO(mikelehen): There may be significant overlap between the mutations
    // affecting these remote documents and the
    // getAllMutationBatchesAffectingQuery() mutations. Consider optimizing.
    let results: DocumentMap;
    return this.remoteDocumentCache
      .getDocumentsMatchingQuery(transaction, query)
      .next(queryResults => {
        return this.computeLocalDocuments(transaction, queryResults);
      })
      .next(promisedResults => {
        results = promisedResults;
        // Now use the mutation queue to discover any other documents that may
        // match the query after applying mutations.
        return this.mutationQueue.getAllMutationBatchesAffectingQuery(
          transaction,
          query
        );
      })
      .next(matchingMutationBatches => {
        let matchingKeys = documentKeySet();
        for (const batch of matchingMutationBatches) {
          for (const mutation of batch.mutations) {
            // TODO(mikelehen): PERF: Check if this mutation actually
            // affects the query to reduce work.
            Eif (!results.get(mutation.key)) {
              matchingKeys = matchingKeys.add(mutation.key);
            }
          }
        }
 
        // Now add in the results for the matchingKeys.
        const promises = [] as Array<PersistencePromise<void>>;
        matchingKeys.forEach(key => {
          promises.push(
            this.getDocument(transaction, key).next(doc => {
              if (doc instanceof Document) {
                results = results.insert(doc.key, doc);
              }
            })
          );
        });
        return PersistencePromise.waitFor(promises);
      })
      .next(() => {
        // Finally, filter out any documents that don't actually match
        // the query.
        results.forEach((key, doc) => {
          if (!query.matches(doc)) {
            results = results.remove(key);
          }
        });
 
        return results;
      });
  }
 
  /**
   * Takes a remote document and applies local mutations to generate the local
   * view of the document.
   * @param transaction The transaction in which to perform any persistence
   *     operations.
   * @param documentKey The key of the document (necessary when remoteDocument
   *     is null).
   * @param document The base remote document to apply mutations to or null.
   */
  private computeLocalDocument(
    transaction: PersistenceTransaction,
    documentKey: DocumentKey,
    document: MaybeDocument | null
  ): PersistencePromise<MaybeDocument | null> {
    return this.mutationQueue
      .getAllMutationBatchesAffectingDocumentKey(transaction, documentKey)
      .next(batches => {
        for (const batch of batches) {
          document = batch.applyToLocalView(documentKey, document);
        }
        return document;
      });
  }
 
  /**
   * Takes a set of remote documents and applies local mutations to generate the
   * local view of the documents.
   * @param transaction The transaction in which to perform any persistence
   *     operations.
   * @param documents The base remote documents to apply mutations to.
   * @return The local view of the documents.
   */
  private computeLocalDocuments(
    transaction: PersistenceTransaction,
    documents: DocumentMap
  ): PersistencePromise<DocumentMap> {
    const promises = [] as Array<PersistencePromise<void>>;
    documents.forEach((key, doc) => {
      promises.push(
        this.computeLocalDocument(transaction, key, doc).next(mutatedDoc => {
          if (mutatedDoc instanceof Document) {
            documents = documents.insert(mutatedDoc.key, mutatedDoc);
          } else Eif (mutatedDoc instanceof NoDocument) {
            documents = documents.remove(mutatedDoc.key);
          } else {
            fail('Unknown MaybeDocument: ' + mutatedDoc);
          }
        })
      );
    });
    return PersistencePromise.waitFor(promises).next(() => documents);
  }
}